#!/usr/bin/python
#-*-coding: utf-8 -*-


from PIL import Image
import math

W_RES  = 320 #Bildbreite
H_RES = 240 #Bildhöhe
BACKGROUND_COLOR = (0,0,0)
ASPECTRATIO = float(W_RES)/H_RES #Seitenverhältnis

class Ray(object):
    def __init__(self, origin, direction):
        self.origin = origin # point
        self.direction = direction.normalized() # vector

    def __repr__(self):
        return 'Ray(%s,%s)' % (repr(self.origin), repr(self.direction))

    def pointAtParameter(self, t):
        return self.origin + self.direction.scale(t)

class Point(object):
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self):
        return 'P(%s,%s,%s)' % (repr(self.x),repr(self.y),repr(self.z))

    def __sub__(self, ptr):
        x = self.x - ptr.x
        y = self.y - ptr.y
        z = self.z - ptr.z
        return Vector(x,y,z)
    
    def __add__(self,vec):
        x = self.x + vec.x
        y = self.y + vec.y
        z = self.z + vec.z
        return Point(x,y,z)

    def isZero(self):
        if self.x != 0 and self.y != 0 and self.z != 0:
            return False
        else:
            return True

class Vector(object):
    def __init__(self, x, y, z, origin = Point(0,0,0)):
        if origin.isZero():
            self.x = x
            self.y = y
            self.z = z
        else:
            self.x = x - origin.x
            self.y = y - origin.y
            self.z = z - origin.z

    def __repr__(self):
        return 'V(%s,%s,%s)' % (repr(self.x),repr(self.y),repr(self.z))

    def __add__(self, vec):
        x = self.x + vec.x
        y = self.y + vec.y
        z = self.z + vec.z
        return Vector(x,y,z)

    def __sub__(self, vec):
        x = self.x - vec.x
        y = self.y - vec.y
        z = self.z - vec.z
        return Vector(x,y,z)

    def __mul__(self, a):
        "Bei übergabe eines Vektors wird Skalarprodukt zurückgegeben, andernfalls multiplikation des Vektor mit Skalar."
        if a is type(Vector):
            return self.x * a.x + self.y * a.y + self.z * a.z
        else:
            x = self.x * a
            y = self.y * a
            z = self.z * a
            return Vector(x,y,z)

    def __div__(self, scalar):
        x = self.x / scalar
        y = self.y / scalar
        z = self.z / scalar
        return Vector(x,y,z)

    def cross(self, b):
        x = self.y * b.z - self.z * b.y
        y = self.z * b.x - self.x * b.z
        z = self.x * b.y - self.y * b.x
        return Vector(x,y,z)

    def length(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

    def normalized(self):
        x = self.x * (1/self.length())
        y = self.y * (1/self.length())
        z = self.z * (1/self.length())
        return Vector(x,y,z)
    
    def scale(self, scalar):
        return self.__mul__(scalar)

    def dot(self,a):
        "Skalarprodukt"
        return self.x * a.x + self.y * a.y + self.z * a.z


class Sphere(object):
    def __init__(self, center, radius, color):
        self.center = center # point
        self.radius = radius # scalar
        self.color = color   # (r,g,b)

    def __repr__(self):
        return 'Sphere(%s,%s)' % (repr(self.center), self.radius)

    def intersectionParameter(self, ray):
        co = self.center - ray.origin       # Vektor von Strahlursprung und Kugelmittelpunk
        v = co.dot(ray.direction)           # berechnung des Winkels
        discriminant =  (self.radius * self.radius) - (co.dot(co) - v*v)
        if discriminant < 0:
            return None
        else:
            return v - math.sqrt(discriminant)

    def normalAt(self, p):
        return (p - self.center).normalized()

    def colorAt(self, ray):
        return self.color
#        # Schnittpunkt ermitteln
#        s = ray.pointAtParameter(self.intersectionParameter(ray))        
#        lightRay = Ray(s,light.origin - ray.pointAtParameter(s))
#
#       #prüfen, ob objekt zwischen Punkt und Lichtquelle
#        for obj in objectlist:
#                   hit = obj.intersectionParameter(lightRay)
#                   if hit:
#                        return (0,0,0)
        


class Plane(object):
    def __init__(self, point, normal, color):
        self.point = point # point
        self.normal = normal.normalized() # vector
        self.color = color   # (r,g,b)

    def __repr__(self):
        return 'Plane(%s,%s)' % (repr(self.point), repr(self.normal))

    def intersectionParameter(self, ray):
        op = ray.origin - self.point
        a = op.dot(self.normal)
        b = ray.direction.dot(self.normal)
        if b:
            return -a/b
        else:
            return None

    def normalAt(self, p):
        return self.normal

    def colorAt(self, ray):
        return self.color

    

class Triangle(object):
    def __init__(self, a, b, c, color):
        self.a = a # point
        self.b = b # point
        self.c = c # point
        self.u = self.b - self.a # direction vector
        self.v = self.c - self.a # direction vector
        self.color = color   # (r,g,b)

    def __repr__(self):
        return 'Triangle(%s,%s,%s)' % (repr(self.a), repr(self.b), repr(self.c))

    def intersectionParameter(self, ray):
        w = ray.origin - self.a
        dv = ray.direction.cross(self.v)
        dvu = dv.dot(self.u)
        if dvu == 0.0:
            return None
        wu = w.cross(self.u)
        r = dv.dot(w) / dvu
        s = wu.dot(ray.direction) / dvu
        if 0<=r and r<=1 and 0<=s and s<=1 and r+s <=1:
            return wu.dot(self.v) / dvu
        else:
            return None
         
    def normalAt(self, p):
        return self.u.cross(self.v).normalized()
 
    def colorAt(self, ray):
        return self.color



class CheckerboardMaterial(object):
    def __init__(self):
        self.baseColor = (1,1,1)
        self.otherColor = (0,0,0)
        self.ambientCoefficient = 1.0 
        self.diffuseCoefficient =  0.6
        self.specularCoefficient = 0.2
        self.checkSize = 1

    def baseColourAt(self, p):
        v = Vector(p)
        v.scale(1.0 / self.checkSize)
        if (int(abs(v.x) + 0.5) + int(abs(v.y) + 0.5) + int(abs(v.z) + 0.5)) % 2:
            return self.otherColour
        return self.baseColour

class Light(object):
    def __init__(self, origin):
        self.origin = origin

class Camera(object):
    def __init__(self, point, direction, angle, ASPECTRATIO):
        self.e = point          #point
        self.c = direction      #point
        self.f = (self.c - self.e) / (self.c - self.e).length()   #vector
        self.s = self.f.cross(Vector(0,1,0)) / Vector.length(self.f.cross(Vector(0,1,0))) #vector
        self.u = self.s.cross(self.f) #vector
        self.alpha = float(angle)/2
        self.height = 2. * math.tan(self.alpha)
        self.width = ASPECTRATIO * self.height

    def calcRay(self,x,y):
        pixelWidth = self.width / (W_RES-1)
        pixelHeight = self.height / (H_RES-1)

        xcomp = self.s * (x * pixelWidth - self.width / 2)
        ycomp = self.u * (y * pixelHeight - self.height / 2)

        return Ray(self.e, self.f + xcomp + ycomp) # evtl mehrere Strahel pro Pixel
        
    
    def rayCasting(self,image):
        print "Berechne Bild..."
        for x in range(W_RES):
            for y in range(H_RES):
                ray = self.calcRay(x,y)
                maxdist = float('inf')
                color = BACKGROUND_COLOR
                for obj in objectlist:
                    hitdist = obj.intersectionParameter(ray)
                    if hitdist:
                        if hitdist < maxdist:
                            maxdist = hitdist
                            color = obj.colorAt(ray)
                image.putpixel((x,H_RES-(y+1)), color)  # y-drehen
        print "Bild berechnet..."
        return image
        

if __name__ == '__main__':
    #Bildobjekt erzeugen
    im = Image.new("RGB",(W_RES,H_RES))
    
    
    # Kamera hinzufügen
    print "Erzeuge Kamera..."
    cam = Camera(Point(0,2,10),Point(0,3,0),45,ASPECTRATIO)
    
    # Lichtquelle erzeugen
    print "Erzeuge Lichtquelle..."
    light = Light(Point(30,30,10))
    
    # Objekte erzeugen
    print "Erzeuge Objekte..."
    objectlist = []
    objectlist.append(Sphere(Point(2.5,3,-10),2,(255,0,0)))           # Kugel: rot
    objectlist.append(Sphere(Point(-2.5,3,-10),2,(0,255,0)))          # Kugel: gruen
    objectlist.append(Sphere(Point(0,7,-10),2,(0,0,255)))             # Kugel: blau
    objectlist.append(Plane(Point(-1,0,0),Vector(0,0,1),(128,128,128)))    # Ebene: Grau

    # Bild berechnen
    cam.rayCasting(im)

    # start
    print "Erstelle Datei..."
    im.save("raycastingPic.png")
    print "Fertig!"






        

        
